دليل شامل لتحسين Pandas DataFrames من حيث استخدام الذاكرة والأداء، يغطي أنواع البيانات والفهرسة والتقنيات المتقدمة.
تحسين Pandas DataFrame: استخدام الذاكرة وضبط الأداء
Pandas هي مكتبة Python قوية لمعالجة البيانات وتحليلها. ومع ذلك، عند العمل مع مجموعات بيانات كبيرة، يمكن أن تستهلك Pandas DataFrames كمية كبيرة من الذاكرة وتظهر أداءً بطيئًا. يقدم هذا المقال دليلاً شاملاً لتحسين Pandas DataFrames لكل من استخدام الذاكرة والأداء، مما يمكّنك من معالجة مجموعات بيانات أكبر بكفاءة أكبر.
فهم استخدام الذاكرة في Pandas DataFrames
قبل الخوض في تقنيات التحسين، من الضروري فهم كيفية تخزين Pandas DataFrames للبيانات في الذاكرة. لكل عمود في DataFrame نوع بيانات محدد، والذي يحدد كمية الذاكرة المطلوبة لتخزين قيمه. تشمل أنواع البيانات الشائعة:
- int64: أعداد صحيحة 64 بت (الافتراضي للأعداد الصحيحة)
- float64: أعداد الفاصلة العائمة 64 بت (الافتراضي لأعداد الفاصلة العائمة)
- object: كائنات Python (تُستخدم للسلاسل النصية وأنواع البيانات المختلطة)
- category: بيانات فئوية (فعالة للقيم المتكررة)
- bool: قيم منطقية (True/False)
- datetime64: قيم التاريخ والوقت
غالبًا ما يكون نوع البيانات object هو الأكثر استهلاكًا للذاكرة لأنه يخزن مؤشرات إلى كائنات Python، والتي يمكن أن تكون أكبر بكثير من أنواع البيانات الأولية مثل الأعداد الصحيحة أو أعداد الفاصلة العائمة. السلاسل النصية، حتى القصيرة منها، عند تخزينها كـ object، تستهلك ذاكرة أكبر بكثير مما هو ضروري. وبالمثل، فإن استخدام int64 عندما يكون int32 كافيًا يهدر الذاكرة.
مثال: فحص استخدام ذاكرة DataFrame
يمكنك استخدام الطريقة memory_usage() لفحص استخدام الذاكرة في DataFrame:
import pandas as pd
import numpy as np
data = {
'col1': np.random.randint(0, 1000, 100000),
'col2': np.random.rand(100000),
'col3': ['A', 'B', 'C'] * (100000 // 3 + 1)[:100000],
'col4': ['This is a long string'] * 100000
}
df = pd.DataFrame(data)
memory_usage = df.memory_usage(deep=True)
print(memory_usage)
print(df.dtypes)
تضمن الوسيطة deep=True حساب استخدام ذاكرة الكائنات (مثل السلاسل النصية) بدقة. بدون deep=True، سيتم حساب الذاكرة للمؤشرات فقط، وليس البيانات الأساسية.
تحسين أنواع البيانات
إحدى أكثر الطرق فعالية لتقليل استخدام الذاكرة هي اختيار أنسب أنواع البيانات لأعمدة DataFrame الخاصة بك. إليك بعض التقنيات الشائعة:
1. تقليل حجم أنواع البيانات الرقمية (Downcasting)
إذا كانت أعمدتك الصحيحة أو ذات الفاصلة العائمة لا تتطلب النطاق الكامل لدقة 64 بت، يمكنك تحويلها إلى أنواع بيانات أصغر مثل int32، int16، float32، أو float16. يمكن أن يقلل هذا بشكل كبير من استخدام الذاكرة، خاصة لمجموعات البيانات الكبيرة.
مثال: ضع في اعتبارك عمودًا يمثل العمر، والذي من غير المرجح أن يتجاوز 120. تخزين هذا كـ int64 هو إهدار؛ سيكون int8 (النطاق من -128 إلى 127) أكثر ملاءمة.
def downcast_numeric(df):
"""Downcasts numeric columns to the smallest possible data type."""
for col in df.columns:
if pd.api.types.is_integer_dtype(df[col]):
df[col] = pd.to_numeric(df[col], downcast='integer')
elif pd.api.types.is_float_dtype(df[col]):
df[col] = pd.to_numeric(df[col], downcast='float')
return df
df = downcast_numeric(df.copy())
print(df.memory_usage(deep=True))
print(df.dtypes)
تُستخدم الدالة pd.to_numeric() مع الوسيطة downcast لتحديد أصغر نوع بيانات ممكن يمكنه تمثيل القيم في العمود تلقائيًا. استخدام copy() يتجنب تعديل DataFrame الأصلي. تحقق دائمًا من نطاق القيم في بياناتك قبل تقليل حجمها لضمان عدم فقدان المعلومات.
2. استخدام أنواع البيانات الفئوية
إذا كان العمود يحتوي على عدد محدود من القيم الفريدة، يمكنك تحويله إلى نوع بيانات category. تخزن أنواع البيانات الفئوية كل قيمة فريدة مرة واحدة فقط، ثم تستخدم رموزًا صحيحة لتمثيل القيم في العمود. يمكن أن يقلل هذا بشكل كبير من استخدام الذاكرة، خاصة للأعمدة التي تحتوي على نسبة عالية من القيم المكررة.
مثال: ضع في اعتبارك عمودًا يمثل رموز البلدان. إذا كنت تتعامل مع مجموعة محدودة من البلدان (على سبيل المثال، دول الاتحاد الأوروبي فقط)، فإن تخزين هذا كفئة سيكون أكثر كفاءة بكثير من تخزينه كسلاسل نصية.
def optimize_categories(df):
"""Converts object columns with low cardinality to categorical type."""
for col in df.columns:
if df[col].dtype == 'object':
num_unique_values = len(df[col].unique())
num_total_values = len(df[col])
if num_unique_values / num_total_values < 0.5:
df[col] = df[col].astype('category')
return df
df = optimize_categories(df.copy())
print(df.memory_usage(deep=True))
print(df.dtypes)
يتحقق هذا الكود مما إذا كان عدد القيم الفريدة في عمود من نوع object أقل من 50% من إجمالي القيم. إذا كان الأمر كذلك، فإنه يحول العمود إلى نوع بيانات فئوي. عتبة 50% هي عتبة اعتباطية ويمكن تعديلها بناءً على الخصائص المحددة لبياناتك. هذا النهج مفيد جدًا عندما يحتوي العمود على العديد من القيم المكررة.
3. تجنب أنواع البيانات Object للسلاسل النصية
كما ذكرنا سابقًا، غالبًا ما يكون نوع البيانات object هو الأكثر استهلاكًا للذاكرة، خاصة عند استخدامه لتخزين السلاسل النصية. إذا أمكن، حاول تجنب استخدام أنواع البيانات object لأعمدة السلاسل النصية. تُفضل الأنواع الفئوية للسلاسل النصية ذات العدد المنخفض من القيم الفريدة. إذا كان عدد القيم الفريدة مرتفعًا، ففكر فيما إذا كان يمكن تمثيل السلاسل برموز رقمية أو ما إذا كان يمكن تجنب بيانات السلسلة النصية تمامًا.
إذا كنت بحاجة إلى إجراء عمليات على السلاسل النصية في العمود، فقد تحتاج إلى إبقائه كنوع object، ولكن فكر فيما إذا كان يمكن إجراء هذه العمليات مقدمًا، ثم تحويله إلى نوع أكثر كفاءة.
4. بيانات التاريخ والوقت
استخدم نوع البيانات `datetime64` لمعلومات التاريخ والوقت. تأكد من أن الدقة مناسبة (قد تكون دقة النانوثانية غير ضرورية). تتعامل Pandas مع بيانات السلاسل الزمنية بكفاءة عالية جدًا.
تحسين عمليات DataFrame
بالإضافة إلى تحسين أنواع البيانات، يمكنك أيضًا تحسين أداء Pandas DataFrames عن طريق تحسين العمليات التي تجريها عليها. إليك بعض التقنيات الشائعة:
1. العمليات الموجهة (Vectorization)
العمليات الموجهة هي عملية إجراء العمليات على مصفوفات أو أعمدة كاملة مرة واحدة، بدلاً من التكرار على العناصر الفردية. تم تحسين Pandas بشكل كبير للعمليات الموجهة، لذا فإن استخدامها يمكن أن يحسن الأداء بشكل كبير. تجنب الحلقات التكرارية الصريحة كلما أمكن ذلك. الدوال المدمجة في Pandas أسرع بشكل عام من حلقات Python المكافئة.
مثال: بدلاً من التكرار عبر عمود لحساب مربع كل قيمة، استخدم الدالة pow():
# Inefficient (using a loop)
import time
start_time = time.time()
results = []
for value in df['col2']:
results.append(value ** 2)
df['col2_squared_loop'] = results
end_time = time.time()
print(f"Loop time: {end_time - start_time:.4f} seconds")
# Efficient (using vectorization)
start_time = time.time()
df['col2_squared_vectorized'] = df['col2'] ** 2
end_time = time.time()
print(f"Vectorized time: {end_time - start_time:.4f} seconds")
عادة ما يكون النهج الموجه أسرع بأضعاف من النهج القائم على الحلقة.
2. استخدام `apply()` بحذر
تتيح لك الطريقة apply() تطبيق دالة على كل صف أو عمود في DataFrame. ومع ذلك، فهي أبطأ بشكل عام من العمليات الموجهة لأنها تتضمن استدعاء دالة Python لكل عنصر. استخدم apply() فقط عندما لا تكون العمليات الموجهة ممكنة.
إذا كان يجب عليك استخدام `apply()`، فحاول جعل الدالة التي تطبقها موجهة قدر الإمكان. فكر في استخدام مزخرف `jit` من Numba لتصريف الدالة إلى كود الآلة لتحسينات كبيرة في الأداء.
from numba import jit
@jit(nopython=True)
def my_function(x):
return x * 2 # Example function
df['col2_applied'] = df['col2'].apply(my_function)
3. تحديد الأعمدة بكفاءة
عند تحديد مجموعة فرعية من الأعمدة من DataFrame، استخدم الطرق التالية للحصول على الأداء الأمثل:
- تحديد مباشر للعمود:
df[['col1', 'col2']](الأسرع لتحديد بضعة أعمدة) - الفهرسة المنطقية:
df.loc[:, [True if col.startswith('col') else False for col in df.columns]](مفيدة لتحديد الأعمدة بناءً على شرط)
تجنب استخدام df.filter() مع التعبيرات النمطية لتحديد الأعمدة، حيث يمكن أن يكون أبطأ من الطرق الأخرى.
4. تحسين عمليات الربط والدمج
يمكن أن يكون ربط ودمج DataFrames مكلفًا حسابيًا، خاصة لمجموعات البيانات الكبيرة. إليك بعض النصائح لتحسين عمليات الربط والدمج:
- استخدام مفاتيح ربط مناسبة: تأكد من أن مفاتيح الربط لها نفس نوع البيانات ومفهرسة.
- تحديد نوع الربط: استخدم نوع الربط المناسب (مثل
inner،left،right،outer) بناءً على متطلباتك. الربط الداخلي أسرع بشكل عام من الربط الخارجي. - استخدام `merge()` بدلاً من `join()`: الدالة
merge()أكثر تنوعًا وغالبًا ما تكون أسرع من الطريقةjoin().
مثال:
df1 = pd.DataFrame({'key': ['A', 'B', 'C', 'D'], 'value1': [1, 2, 3, 4]})
df2 = pd.DataFrame({'key': ['B', 'D', 'E', 'F'], 'value2': [5, 6, 7, 8]})
# Efficient inner join
df_merged = pd.merge(df1, df2, on='key', how='inner')
print(df_merged)
5. تجنب نسخ DataFrames دون داعٍ
تنشئ العديد من عمليات Pandas نسخًا من DataFrames، والتي يمكن أن تكون مستهلكة للذاكرة وتستغرق وقتًا طويلاً. لتجنب النسخ غير الضروري، استخدم الوسيطة inplace=True عند توفرها، أو قم بتعيين نتيجة العملية مرة أخرى إلى DataFrame الأصلي. كن حذرًا جدًا مع inplace=True حيث يمكن أن يخفي الأخطاء ويجعل تصحيح الأخطاء أكثر صعوبة. غالبًا ما يكون من الآمن إعادة التعيين، حتى لو كان أقل أداءً بقليل.
مثال:
# Inefficient (creates a copy)
df_filtered = df[df['col1'] > 500]
# Efficient (modifies the original DataFrame in place - CAUTION)
df.drop(df[df['col1'] <= 500].index, inplace=True)
#SAFER - reassigns, avoids inplace
df = df[df['col1'] > 500]
6. التقطيع والتكرار
بالنسبة لمجموعات البيانات الكبيرة جدًا التي لا يمكن احتواؤها في الذاكرة، فكر في معالجة البيانات على شكل أجزاء (chunks). استخدم المعلمة `chunksize` عند قراءة البيانات من الملفات. كرر عبر الأجزاء وقم بإجراء تحليلك على كل جزء على حدة. يتطلب هذا تخطيطًا دقيقًا لضمان بقاء التحليل صحيحًا، حيث تتطلب بعض العمليات معالجة مجموعة البيانات بأكملها مرة واحدة.
# Read CSV in chunks
for chunk in pd.read_csv('large_data.csv', chunksize=100000):
# Process each chunk
print(chunk.shape)
7. استخدام Dask للمعالجة المتوازية
Dask هي مكتبة حوسبة متوازية تتكامل بسلاسة مع Pandas. تتيح لك معالجة DataFrames الكبيرة بالتوازي، مما يمكن أن يحسن الأداء بشكل كبير. تقسم Dask الـ DataFrame إلى أقسام أصغر وتوزعها عبر أنوية متعددة أو أجهزة.
import dask.dataframe as dd
# Create a Dask DataFrame
ddf = dd.read_csv('large_data.csv')
# Perform operations on the Dask DataFrame
ddf_filtered = ddf[ddf['col1'] > 500]
# Compute the result (this triggers the parallel computation)
result = ddf_filtered.compute()
print(result.head())
الفهرسة لعمليات بحث أسرع
إنشاء فهرس على عمود يمكن أن يسرع بشكل كبير عمليات البحث والتصفية. تستخدم Pandas الفهارس لتحديد موقع الصفوف التي تطابق قيمة معينة بسرعة.
مثال:
# Set 'col3' as the index
df = df.set_index('col3')
# Faster lookup
value = df.loc['A']
print(value)
# Reset the index
df = df.reset_index()
ومع ذلك، يمكن أن يؤدي إنشاء عدد كبير جدًا من الفهارس إلى زيادة استخدام الذاكرة وإبطاء عمليات الكتابة. أنشئ فهارس فقط على الأعمدة التي يتم استخدامها بشكل متكرر لعمليات البحث أو التصفية.
اعتبارات أخرى
- الأجهزة (Hardware): فكر في ترقية أجهزتك (وحدة المعالجة المركزية، ذاكرة الوصول العشوائي، قرص الحالة الصلبة) إذا كنت تعمل باستمرار مع مجموعات بيانات كبيرة.
- البرامج (Software): تأكد من أنك تستخدم أحدث إصدار من Pandas، حيث غالبًا ما تتضمن الإصدارات الأحدث تحسينات في الأداء.
- التنميط (Profiling): استخدم أدوات التنميط (مثل
cProfile،line_profiler) لتحديد اختناقات الأداء في الكود الخاص بك. - تنسيق تخزين البيانات: فكر في استخدام تنسيقات تخزين بيانات أكثر كفاءة مثل Parquet أو Feather بدلاً من CSV. هذه التنسيقات عمودية وغالبًا ما تكون مضغوطة، مما يؤدي إلى أحجام ملفات أصغر وأوقات قراءة/كتابة أسرع.
الخاتمة
يعد تحسين Pandas DataFrames من حيث استخدام الذاكرة والأداء أمرًا بالغ الأهمية للعمل مع مجموعات البيانات الكبيرة بكفاءة. من خلال اختيار أنواع البيانات المناسبة، واستخدام العمليات الموجهة، وفهرسة بياناتك بشكل فعال، يمكنك تقليل استهلاك الذاكرة بشكل كبير وتحسين الأداء. تذكر أن تقوم بتنميط الكود الخاص بك لتحديد اختناقات الأداء وفكر في استخدام التقطيع أو Dask لمجموعات البيانات الكبيرة للغاية. من خلال تطبيق هذه التقنيات، يمكنك إطلاق العنان للإمكانات الكاملة لـ Pandas لتحليل البيانات ومعالجتها.